/**
 * HashMapRoutingTable.java
 *
 * @author Hagen Boehm
 */


package SSF.OS.OSPFv2;


import java.util.*;
import SSF.Net.Util.IP_s;


// ========================= class SSF.OS.OSPFv2.HashMapRoutingTable ======================== //
/**
 * An implementation of OSPF routing table usingn HashMaps to distinguish between router and
 * network entries.
 */

public class HashMapRoutingTable {

  // .............................. member data .............................. //

  /** A HashMap storing routing table entries with destination "router". If destination is
   *  an area border router or an AS boundary router and there is a valid entry, it will be
   *  found in this structure. Keys are Integers representing the routers router ID. The
   *  associated value is also a HashMap, matching area IDs (key) to RouterEntry objects 
   *  (value). This is because their may be several equal-cost paths to the same router 
   *  through several areas, if two ABRs or ASBRs share multiple areas in common.
   */
  public HashMap routers;

  /** A HashMap storing routing table entries with destination "network". All valid network
   *  entries can be found here. Keys are Strings consisting of destination ID and address
   *  mask, separated by a slash. The associated value is a NetworkEntry.
   */
  public HashMap networks;

  /** A link to the OSPF session this routing table belongs to. */
  public OSPF myOSPF;


  // ------------------------------ constructor ------------------------------- //
  /** Create a new OSPF Routing Table formed by two HashMaps. One for router entries
   *  and one for network entries.
   */
  public HashMapRoutingTable(OSPF o) {
    myOSPF = o;
    routers = new HashMap();
    networks = new HashMap();
  }

  // ---------------------------- HashMapRoutingTable.add ------------------------- //
  /** add a new entry (RouterEntry) to the routing table whose destination is a router.
   *  @param newREntry RouterEntry to be added.
   *  @param key Integer representing the destinations router's ID.
   *  @param areaID Integer representing area's ID whose link state information has led
   *                to the RouterEntry.
   */
  public void add(RouterEntry newREntry, Integer key, Integer areaID) { 
    if (routers.containsKey(key)) {
      ((HashMap)routers.get(key)).put(areaID, newREntry);
    } else {
      HashMap entry = new HashMap();
      entry.put(areaID, newREntry);
      routers.put(key, entry);
    }
  }

  // ---------------------------- HashMapRoutingTable.add ------------------------- //
  /** add a new entry (NetworkEntry) to the routing table whose destination is a network.
   *  @param newNEntry NetworkEntry to be added.
   *  @param key String consisting of an ip address followed by a slash followed by a
   *             network mask. It uniquely identifys the NetworkEntry.
   */
  public void add(NetworkEntry newNEntry, String key) {
    networks.put(key, newNEntry);
  }

  // -------------------------- HashMapRoutingTable.getDest ------------------------------ //
  /** Lookup the routing table for a NetworkEntry and return it if it exists.
   *  @param destID String, representing the identifier of the network entry.
   *  @return NetworkEntry or null if there is no entry.
   */
  public NetworkEntry getDest(String destID) {
    return (NetworkEntry)networks.get(destID);
  }

  // ------------------------- HashMapRoutingTable.getDest ------------------------------ //
  /** Lookup the routing table for a particular RouterEntry and return it if it exists.
   *  @param destID Integer, representing the identifier of the router entry.
   *  @param destArea Integer, representing the id of the area who is responsible for the entry.
   *  @return RouterEntry or null if there is no entry.
   */
  public RouterEntry getDest(Integer destID, Integer destArea) {
    if (routers.containsKey(destID)) {
      HashMap entry = (HashMap)routers.get(destID);
      return (RouterEntry)entry.get(destArea);
    } else {
      return null;
    }
  }

  // ------------------------- HashMapRoutingTable.getNetCost --------------------------- //
  /** Return the cost of a NetworkEntry Object specified by its key String. If there is no
   *  such entry, return -1.
   *  @param netKey String which uniquely identifies the NetworkEntry if it exists.
   *  @param areaID id of the area in which a new Summary LSA should be installed.
   *  @return int value representing the cost of the shortest paths to the destination network
   *          if there is such a destination, else -1.
   */
  public int getNetCost(String netKey, int areaID) {
    if (networks.containsKey(netKey)) {
      // network entry exists
      NetworkEntry netEnt = (NetworkEntry)networks.get(netKey);
      if (netEnt.cost >= OSPF.LS_INFINITY) return -1;
      // do not announce inter area paths into the backbone!
      if (netEnt.pathType == OSPF.INTER_AREA && areaID == 0) return -1;
      if ((netEnt.pathType == OSPF.INTRA_AREA || netEnt.pathType == OSPF.INTER_AREA) && 
          netEnt.area != areaID) return netEnt.cost;
    }
    if (netKey.equals("0/0")) {
      // request for default route cost.
      AreaData requestArea = (AreaData)myOSPF.areaStructs.get(new Integer(areaID));
      if (!requestArea.options[6] && requestArea.stubDefaultCost > 0) 
        return requestArea.stubDefaultCost;
      else
	return -1;
    }
    // network entry does not exist or can not be used to generate a Summary LSA
    return -1;
  }

  // ---------------------- HashMapRoutingTable.getRoutCost -------------------------- //
  /** Return the cost of the RouterEntry Object describing the preferred paths to an AS
   *  Boundary Router. If there is no such entry, return -1.
   *  @param routKey Integer representing the ASBR's router id.
   *  @param areaID id of the area in which a new Summary LSA should be installed.
   *  @return int value representing the cost of the preferred shortest paths (according
   *          to RFC 2328 chapter 16.4 step 3) to the ASBR if exist, else -1.
   */
  public int getRoutCost(Integer routKey, int areaID) {
    Integer prefAreaID = getPrefASBREntryArea(routKey);
    if (prefAreaID.intValue() != -1) {
      RouterEntry routEnt = (RouterEntry)getDest(routKey, prefAreaID);
      if (routEnt.cost >= OSPF.LS_INFINITY) return -1;
      // do not announce inter area paths into the backbone!
      if (routEnt.pathType == OSPF.INTER_AREA && areaID == 0) return -1;
      if ((routEnt.pathType == OSPF.INTRA_AREA || routEnt.pathType == OSPF.INTER_AREA) && 
          prefAreaID.intValue() != areaID) return routEnt.cost;
    }
    // router entry does not exist or can not be used to generate a Summary LSA.
    return -1;
  }

  // ---------------------- HashMapRoutingTable.getPrefASBREntryArea ----------------- //
  /** return the id of the area which stores the Summary LSA that lead to the preferred 
   *  RouterEntry leading to a particular AS Boundary router. The preferred path is 
   *  determined as described in RFC 2328 chapter 16.4 step 3. If there is no such entry,
   *  return -1.
   *  @param key Integer Object indicating the AS Boundary router's router id.
   *  @return Integer representing the area's id or -1 if there is no entry.
   */
  public Integer getPrefASBREntryArea(Integer key) {
    Integer nextAreaID;
    Integer currAreaID = new Integer(-1);
    int currCost = OSPF.LS_INFINITY + 1;
    RouterEntry nextRouter;
    if (routers.containsKey(key)) {
      HashMap entrys = (HashMap)routers.get(key);
      for (Iterator entryKeys = entrys.keySet().iterator(); entryKeys.hasNext(); ) {
	nextAreaID = (Integer)entryKeys.next();
        nextRouter = (RouterEntry)entrys.get(nextAreaID);
        if (!nextRouter.isASBoundary) continue;
        if (nextRouter.cost < currCost) {
          currCost = nextRouter.cost;
          currAreaID = nextAreaID;
	} else {
          if (nextRouter.cost == currCost && nextAreaID.intValue() > currAreaID.intValue())
            currAreaID = nextAreaID;
        }
      }
    }
    return currAreaID;
  }

  // ---------------------------- HashMapRoutingTable.delNet ------------------------ //
  /** remove an entry from HashMap "networks". This Method is used, when the table became
   *  invalid to delete networks which were modified. The remaining network entries then
   *  must be invalid and should be deleted from the IP forwarding table.
   *  @param key String Object, representing the key value of the network destination.
   */
  public void delNet(String key) {
    networks.remove(key);
  }

  // ---------------------------- HashMapRoutingTable.delRout ------------------------ //
  /** remove a particular route from HashMap "routers". This Method is used to delete an
   *  unreachable router route from the routing table.
   *  @param key Integer Object, representing the key value of the router destination.
   *  @param aID Integer Object, representing the id of the area who has lead to this entry.
   */
  public void delRout(Integer key, Integer aID) {
    HashMap entry = (HashMap)routers.get(key);
    if (entry != null) {
      entry.remove(aID);
      if (entry.isEmpty()) routers.remove(key);
    }
  }

  // ------------------------- HashMapRoutingTable.delRouter -------------------------- //
  /** remove ALL routes to a particular router. This method is used to delete AS Boundary
   *  Routers from an invalidated routing table.
   *  @param key Integer Object, representing the id of the router to be removed.
   */
  public void delRouter(Integer key) {
    routers.remove(key);
  }

  // ------------------------- HashMapRoutingTable.getNetKeys ------------------------- //
  /** return the keys of all network entries. The method is used when this table is
   *  invalidated. While calculating the new routes, all routes which remains the same
   *  or have changed in the new routing table are deleted from this table by method
   *  "delNet()". The remaining routes serve as a mnemonic for routes which became 
   *  unreachable and so can be removed from ip forwarding table accurately.
   *  @return Object[] containing the destination keys of the remaining network entries.
   */
  public Object[] getNetKeys() {
    return networks.keySet().toArray();
  }

  // ------------------------ HashMapRoutingTable.getRoutKeys ------------------------- //
  /** return the key values of all router entries. The method is used to determine routes
   *  to AS boundary routers and to update and send summary LSAs of type 4.
   *  @return Iterator containing the destination keys of all RouterEntry Objects.
   */
  public Iterator getRoutKeys() {
    return routers.keySet().iterator();
  }
  
  // --------------------------- HashMapRoutingTable.print -------------------------- //
  /** print this Table in a form following the examples of RFC 2328 chapter 11. */
  public void print() {
    String nextNetKey;
    Integer nextRoutKey;
    NetworkEntry nextNetEntry;
    RouterEntry nextRoutEntry;
    ShortestPath sp;
    HashMap areas;
    String nextLine;
    final String TAB = "                                                                 ";
    if (networks.isEmpty() && routers.isEmpty()) {
      myOSPF.printMessage("|Routing Table recalculated! Content is now: Empty!");
      return;
    }
    myOSPF.printMessage("|Routing Table recalculated! Content is now:");
    System.out.println("Type       Dest       Area    Path Type    Cost    ViaIntf/NextHop(s)     Adv. Router(s)");
    System.out.println("----------------------------------------------------------------------------------------");
    // print all network entries at first.
    for (Iterator itNetKeys = networks.keySet().iterator(); itNetKeys.hasNext(); ) {
      nextNetKey = (String)itNetKeys.next();
      nextNetEntry = (NetworkEntry)networks.get(nextNetKey);
      // construct a String of the next network entry to be print.
      nextLine = "N" + "     " + myOSPF.convertRouteID(nextNetKey) + "       " + nextNetEntry.area +
                 "     " + getPathType(nextNetEntry.pathType) + "      " + nextNetEntry.cost + "     ";
      String tab = TAB.substring(0, nextLine.length());
      // add all next hops of the network entry and print them line by line.
      for (Enumeration e = nextNetEntry.equalCostPaths.elements(); e.hasMoreElements(); ) {
	sp = ((ShortestPath)e.nextElement());
        nextLine += IP_s.IPtoString(sp.getViaIntf()) + "/";
        if (sp.getNHopIP() == 0) nextLine += "***";
        else nextLine += IP_s.IPtoString(sp.getNHopIP());
        if (nextNetEntry.pathType == OSPF.INTER_AREA) {
          nextLine += "        " + IP_s.IPtoString(sp.advRouter);
        } else {
          nextLine += "          ***";
        }
        System.out.println(nextLine);
        nextLine = tab;
      }
    }
    // now print all router entries.
    String dest;
    for (Iterator itRoutKeys = routers.keySet().iterator(); itRoutKeys.hasNext(); ) {
      nextRoutKey= (Integer)itRoutKeys.next();
      dest = IP_s.IPtoString(nextRoutKey.intValue());
      areas = (HashMap)routers.get(nextRoutKey);
      Integer areaID;
      for (Iterator itAreas = areas.keySet().iterator(); itAreas.hasNext(); ) {
	areaID = (Integer)itAreas.next();
        nextRoutEntry =  (RouterEntry)areas.get(areaID);
        // construct a String of the next router entry to be print.
        nextLine = "R" + "     " + dest + "/32" + "       " + areaID.intValue() + "     " +
                   getPathType(nextRoutEntry.pathType) + "      " + nextRoutEntry.cost + "     ";
        if (nextRoutEntry.equalCostPaths.isEmpty()) {
          nextLine += "no next hop!            ***  ";
          System.out.println(nextLine);
        } else {
          String tab = TAB.substring(0, nextLine.length());
          // add all next hops of the router entry and print them line by line.
          for (Enumeration e = nextRoutEntry.equalCostPaths.elements(); e.hasMoreElements(); ) {
            sp = ((ShortestPath)e.nextElement());
            nextLine += IP_s.IPtoString(sp.getViaIntf()) + "/";
            if (sp.getNHopIP() == 0) nextLine += "***";
            else nextLine += IP_s.IPtoString(sp.getNHopIP());
            if (nextRoutEntry.pathType == myOSPF.INTER_AREA) {
              nextLine += "      " + IP_s.IPtoString(sp.advRouter);
            } else nextLine += "          ***";
            System.out.println(nextLine);
            nextLine = tab;
          }
        }
      }
    }
    System.out.println("----------------------------------------------------------------------------------------\n");
  }

  // ------------------------ HashMapRoutingTable.getPathType -------------------------- //
  /** Convert the given path type in a String. */
  public String getPathType(byte type) {
    switch (type) {
    case OSPF.INTRA_AREA:
      return ("intra-area");
    case OSPF.INTER_AREA:
      return ("inter-area");
    case OSPF.TYPE_1_EXTERNAL:
      return ("type 1 ext.");
    case OSPF.TYPE_2_EXTERNAL:
      return ("type 2 ext.");
    default:
      return ("");
    }
  }

} // end of class HashMapRoutingTable
